/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.    
 */
package dk.redillusions.jdbc.proxy;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.Properties;

/**
 * <p>
 * Implementation of the {@link Driver} interface which basically proxies calls
 * to an underlying implementation (such as the mysql driver).
 * </p>
 * <p>
 * In order to use the {@link ConfiguredProxyDriver} a configuration file named
 * <code>{@value #SETTINGS_FILE_NAME}</code> must be available in the class-path
 * and must contain the <code>{@value #DRIVER_CLASS_PROPERTY}</code> property
 * with the class name of the actual driver class to use (for example
 * <code>{@value #DRIVER_CLASS_PROPERTY}=com.mysql.jdbc.Driver</code> (without
 * quotes) in order to use the MySQL driver as the underlying jdbc driver). The
 * connect URL (as passed to {@link #connect(String, Properties)} and
 * {@link #acceptsURL(String)}) must start with {@value #JDBC_PREFIX}. The rest
 * of the URL should be the same as if not using the jdbc-proxy.
 * </p>
 * 
 * @author Sune Wettersteen
 * 
 */
public final class ConfiguredProxyDriver implements Driver {
    /**
     * The part of the {@link #JDBC_PREFIX} which identifies the
     * {@link ParameterizedProxyDriver}.
     */
    private static final String PROX_PREFIX = "prox:";

    /**
     * The prefix for the {@link ParameterizedProxyDriver} connection.
     */
    public static final String JDBC_PREFIX = "jdbc:" + PROX_PREFIX;

    static {
        try {
            DriverManager.registerDriver(new ConfiguredProxyDriver());
        } catch (SQLException e) {
            throw new RuntimeException("Failed to register driver", e);
        }
    }

    /**
     * Name of the properties file containing the driver configuration.
     */
    public static final String SETTINGS_FILE_NAME = "jdbc-proxy.properties";

    /**
     * Name of the required property which contains information about which
     * driver class should be used for the underlying driver which this driver
     * proxies to.
     */
    public static final String DRIVER_CLASS_PROPERTY = "jdbc-proxy.driverclass";

    /**
     * The underlying driver.
     */
    private final Driver underlyingDriver;

    /**
     * Constructor. Makes sure to read the configuration file and instantiate
     * the correct underlying driver.
     */
    @SuppressWarnings("unchecked")
    public ConfiguredProxyDriver() {
        // Load the configuration
        final Properties configuration = this.loadConfiguration();

        // Get the underlying driver class name
        final String driverClassName = configuration.getProperty(DRIVER_CLASS_PROPERTY);
        if (driverClassName == null) {
            throw new ConfigurationException("Missing configuration property " + DRIVER_CLASS_PROPERTY);
        }

        try {
            // Get the driver class
            final Class<Driver> driverClass = (Class<Driver>) Class.forName(driverClassName);

            // Instantiate the driver
            final Driver driver = driverClass.newInstance();

            // Register the driver
            DriverManager.registerDriver(driver);

            this.underlyingDriver = driver;
        } catch (ClassNotFoundException e) {
            throw new ConfigurationException("Configured driver " + driverClassName + " not found", e);
        } catch (InstantiationException e) {
            throw new RuntimeException("Failed to instantiate driver class: " + driverClassName, e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Failed to instantiate driver class: " + driverClassName, e);
        } catch (SQLException e) {
            throw new RuntimeException("Failed to register driver class: " + driverClassName, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean acceptsURL(final String url) throws SQLException {
        if (!url.startsWith(JDBC_PREFIX)) {
            return false;
        }

        final String strippedConnectURL = this.getDatabaseConnectURL(url);

        return this.underlyingDriver.acceptsURL(strippedConnectURL);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Connection connect(final String url, final Properties info) throws SQLException {
        // Validate
        if (!url.startsWith(JDBC_PREFIX)) {
            return null;
        }

        // Get stripped connect url
        final String strippedConnectURL = this.getDatabaseConnectURL(url);

        // Obtain the actual connection from the driver
        final Connection connection = this.underlyingDriver.connect(strippedConnectURL, info);
        // If the connection was null just return it
        if (connection == null) {
            return connection;
        }

        /*
         * Create the proxy connection which is sent to the connection
         * invocation handler
         */
        final ProxyConnection proxyConnection = new ProxyConnection(connection);

        // Create the invocation handler for the proxy
        final ConnectionInvocationHandler connectionInvocationHandler = new ConnectionInvocationHandler(proxyConnection);

        for (JDBCProvider provider : JDBCProviderRegistry.getRegistryList()) {
        	// Add delegate
        	connectionInvocationHandler.addConnection(provider.provideConnection());
        }

        // Create and return the connection proxy
        final Connection connectionProxy = (Connection) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class<?>[] {
            Connection.class
        }, connectionInvocationHandler);

        return connectionProxy;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public DriverPropertyInfo[] getPropertyInfo(final String url, final Properties info) throws SQLException {
        return this.underlyingDriver.getPropertyInfo(url, info);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean jdbcCompliant() {
        return this.underlyingDriver.jdbcCompliant();
    }

    /**
     * This will reflect the major version of the {@link ConfiguredProxyDriver},
     * and not that of the underlying driver.
     * 
     * @return this driver's major version number
     */
    @Override
    public int getMajorVersion() {
        return 1;
    }

    /**
     * This will reflect the minor version of the {@link ConfiguredProxyDriver},
     * and not that of the underlying driver.
     * 
     * @return this driver's minor version number
     */
    @Override
    public int getMinorVersion() {
        return 0;
    }

    /**
     * Convert the connect URL for use with the underlying driver.
     * 
     * @param connectURL
     *            The original connect URL containing {@value #JDBC_PREFIX}.
     * @return The connect URL with {@value #PROX_PREFIX} removed.
     */
    private String getDatabaseConnectURL(final String connectURL) {
        return connectURL.replace(PROX_PREFIX, "");
    }

    /**
     * Load the settings from the configuration properties (
     * {@value #SETTINGS_FILE_NAME}) file.
     * 
     * @return The properties
     */
    private Properties loadConfiguration() {
        // Get the URL of the file
        InputStream inputStream = ClassLoader.getSystemResourceAsStream(SETTINGS_FILE_NAME);
        // Check if the file exists
        if (inputStream == null) {
            // Alternatively try this class' classloader
            inputStream = this.getClass().getClassLoader().getResourceAsStream(SETTINGS_FILE_NAME);

            if (inputStream == null) {
                // File wasn't found
                throw new RuntimeException("Could not find configuration resource: " + SETTINGS_FILE_NAME);
            }
        }

        try {
            /*
             * Create properties and load the data from the input stream into
             * the properties
             */
            final Properties properties = new Properties();
            properties.load(inputStream);

            return properties;
        } catch (IOException e) {
            // Failed to load properties from file
            throw new RuntimeException("Failed to read properties from configuration resource: " + SETTINGS_FILE_NAME, e);
        } finally {
            // Make sure to close the input stream
            try {
                inputStream.close();
            } catch (IOException e) {
                // Failed to close the input stream
                throw new RuntimeException("Failed to close configuration file input stream for configuration resource: " + SETTINGS_FILE_NAME, e);
            }
        }
    }

}
